泛型(Generics)是 Java 程式語言中的一個重要特性,允許在定義類別、介面和方法時使用類型參數。也就是說,泛型就是參數化類型,使得程式碼可以適用於多種資料類型,而不需要對每種類型都寫一次。
泛型的重要性體現在以下幾個方面:
類型安全:泛型在編譯時提供類型檢查,減少運行時錯誤的可能性。
程式碼重用:通過使用泛型,可以編寫出更通用、更靈活的程式碼,適用於多種資料類型。
性能提升:泛型消除許多顯式類型轉換的需要,提高程式的執行效率。
可讀性增強:泛型使得程式碼意圖更加明確,提高程式碼的可讀性和可維護性。
API 設計:泛型為庫設計者提供更強大的工具,使得 API 更加靈活和易於使用。
泛型的核心思想是將類型參數化。在 Java 中,泛型主要應用於類別、介面和方法。讓我們來解泛型的基本概念:
泛型類別是在類別名稱後使用尖括號 <>
來定義一個或多個類型參數的類別。例如:
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
在這個例子中,T
是類型參數,可以在創建 Box
物件時指定具體的類型。
泛型方法是在返回類型前使用 <>
聲明類型參數的方法。例如:
public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.print(element + " ");
}
System.out.println();
}
這個方法可以印出任何類型的陣列。
雖然可以使用任何有效的標識符作為類型參數名,但通常遵循以下慣例:
這些命名慣例有助於提高程式碼的可讀性,特別是在處理多個類型參數時。
泛型為 Java 程式設計帶來許多顯著的優點,使得程式碼更加安全、高效和可重用。以下是泛型的主要優點:
泛型提供編譯時的類型檢查,這意味著許多錯誤可以在編譯階段就被發現,而不是在運行時才出現,換言之,減少運行時錯誤,提高程式的穩定性。例如:
List<String> list = new ArrayList<>();
list.add("Hello");
list.add(123); // 編譯錯誤:不能添加整數到字串列表
在使用泛型之前,從集合中取出元素時常常需要進行顯式的類型轉換。泛型消除這種需要:
// 不使用泛型
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 需要類型轉換
// 使用泛型
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 不需要類型轉換
可使程式碼更加簡潔,也消除因類型轉換錯誤而可能產生的 ClassCastException。
泛型允許我們編寫更通用的程式碼,可以適用於多種類型,提高程式碼的重用性:
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// getters and setters
}
Pair
類別可以用於任何類型的鍵值對,無需為每種類型組合都創建一個新的類別。
泛型在 Java 中有廣泛的應用,特別是在集合框架中。讓我們來看看泛型的幾種常見使用方式:
Java 集合框架大量使用泛型,這使得集合的使用更加安全和方便:
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(123); // 編譯錯誤
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
我們可以創建自己的泛型類別,以增加程式碼的靈活性:
public class Pair<T, U> {
private T first;
private U second;
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() { return first; }
public U getSecond() { return second; }
}
// 使用
Pair<String, Integer> pair = new Pair<>("Hello", 42);
String first = pair.getFirst(); // "Hello"
int second = pair.getSecond(); // 42
泛型方法可以獨立於類別而存在:
public class Utilities {
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static <T extends Comparable<T>> T findMax(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
// 使用
Integer[] numbers = {1, 2, 3, 4, 5};
Utilities.swap(numbers, 0, 4); // 交換第一個和最後一個元素
String max = Utilities.findMax("apple", "banana"); // 返回 "banana"
例子中,swap
方法可以交換任何類型陣列的元素,而 findMax
方法可以比較任何實現 Comparable
介面的類型。
雖然泛型為 Java 程式設計帶來許多好處,但也有一些限制,以下是泛型的主要限制:
Java 的泛型是通過類型擦除(Type Erasure)實現的。這意味著泛型資訊只在編譯時存在,運行時會被擦除。例如:
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // 輸出:true
這兩個 List 在運行時實際上是相同的類型。這種機制保證向後兼容性,但也帶來一些限制。
由於類型擦除,不能直接創建泛型類型的陣列。例如:
// 這是不合法的
T[] array = new T[10]; // 編譯錯誤
// 但可以這樣做
T[] array = (T[]) new Object[10]; // 需要類型轉換,可能產生 ClassCastException
這個限制是因為陣列需要在運行時知道確切的元素類型,而泛型資訊在運行時已經被擦除。
泛型不能使用基本數據類型(如 int, double, char 等)作為類型參數。必須使用對應的包裝類(如 Integer, Double, Character 等)。
// 不合法
List<int> numbers = new ArrayList<>(); // 編譯錯誤
// 正確的做法
List<Integer> numbers = new ArrayList<>();
泛型類型的靜態成員不能使用類的類型參數。例如:
public class GenericClass<T> {
private static T staticMember; // 編譯錯誤
public static T getStaticMember() { // 編譯錯誤
return null;
}
}
這是因為靜態成員屬於類本身,而不是類的實例,在類加載時就已經初始化,此時類型參數還未確定。
泛型萬用字元是 Java 泛型中的一個重要概念,提供更大的靈活性,特別是在處理不同但相關的泛型類型時。Java 中有三種主要的萬用字元:無界萬用字元、上界萬用字元和下界萬用字元。
無界萬用字元用問號 ?
表示,代表任何類型。當你只關心操作而不關心具體類型時,可以使用無界萬用字元。
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> strList = Arrays.asList("Hello", "World");
printList(intList); // 輸出:1 2 3
printList(strList); // 輸出:Hello World
上界萬用字元限制未知類型必須是指定類型 T 或其子類型。這在讀取具體元素時很有用。
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sumOfList(intList)); // 輸出:6.0
System.out.println(sumOfList(doubleList)); // 輸出:6.6
下界萬用字元的限制是,未知類型必須是指定類型 T 或其父類型 ->這在寫入元素時很有用。
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}
// 使用
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // 輸出:[1, 2, 3, 4, 5]
萬用字元的使用可以增加程式碼的靈活性,但也需要注意:
? extends T
) 的集合是只讀的,你不能添加元素到這樣的集合中。? super T
) 的集合允許添加元素,但從中讀取元素時只能當作 Object 類型。當涉及到泛型類別的繼承時,需要注意以下幾點:
例如:
class GenericParent<T> {
T value;
// ...
}
class GenericChild<T, U> extends GenericParent<T> {
U anotherValue;
// ...
}
簡單來說,雖然 Integer
是 Number
的子類,但 GenericParent<Integer>
並不是 GenericParent<Number>
的子類。這種情況稱為泛型不變性(invariance),也就是說,泛型類別的類型參數不會自動轉換。
當覆寫泛型類別中的方法時,方法簽名必須完全匹配。例如:
class Animal {
public <T> void feed(T food) {
// ...
}
}
class Dog extends Animal {
@Override
public <T> void feed(T food) {
// 正確的覆寫
}
}
如果嘗試改變類型參數,編譯器會報錯:
class Cat extends Animal {
@Override
public void feed(String food) {
// 編譯錯誤:這不是一個有效的覆寫
}
}
為處理泛型和繼承之間的關係,我們經常需要使用萬用字元:
List<? extends Number> numbers = new ArrayList<Integer>();
// 可以讀取,但不能添加元素(除 null)
Number n = numbers.get(0); // 可以
// numbers.add(Integer.valueOf(1)); // 編譯錯誤
List<? super Integer> integers = new ArrayList<Number>();
// 可以添加 Integer 或其子類型,但讀取時只能當作 Object
integers.add(Integer.valueOf(1)); // 可以
// Integer i = integers.get(0); // 編譯錯誤
Object obj = integers.get(0); // 可以
以下是一些重要的泛型使用建議:
遵循標準的泛型命名慣例可以提高程式碼的可讀性:
雖然泛型很強大,但過度使用可能會使程式碼變得複雜難懂。只在真正需要的地方使用泛型。
由於泛型陣列創建的限制,通常建議使用 List<T>
而不是 T[]
。
// 避免這樣做
T[] array = (T[]) new Object[10]; // 可能導致 ClassCastException
// 推薦這樣做
List<T> list = new ArrayList<>(10);
在 Java 7 及以後版本中,使用菱形運算符 <>
可以簡化泛型程式碼:
Map<String, List<String>> map = new HashMap<>(); // 而不是 new HashMap<String, List<String>>()
? extends T
當你只需要從結構中讀取。? super T
當你只需要寫入結構。?
當你既不需要讀也不需要寫具體類型。如果只有方法需要類型參數,使用泛型方法而不是使整個類泛型化。
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
本篇文章同步刊載: JYI.TW
筆者個人的網站: JUNYI